// String substitution datapump
// Copyright 2006 (C) Ralph Thomas

#include <datapumps/substitute.h>
#include <binder.hpp>
#include <string>
#include <map>
#include <vector>
#include <stdexcept>
#include <adobe/name.hpp>
#include <sstream>

namespace datapumps {

	class substitute_pump_t : public adobe::binder_t {
		typedef std::map<adobe::name_t, std::string>	substitute_map_t;
		typedef std::vector<adobe::binder_helper_t*>	helper_list_t;
		substitute_map_t				subst_map_m;
		std::string					dest_m;
		std::string					last_m;
		bool						initializing_m;
		helper_list_t					helpers_m;
		adobe::sheet_t::monitor_value_t			value_monitor_m;
		adobe::sheet_t::monitor_active_t		active_monitor_m;
		//
		// Stuff required for copying.
		//
		adobe::sheet_t&					sheet_m;
		adobe::dictionary_t				arguments_m;
		//
		/// Substitutes a map of names into a string, using the "@name;" form
		/// to read in the names. Names may be ignored by doing "\@name;" which
		/// will be output as "@name;".
		///
		/// \param	s	the string to substitute into.
		/// \param	d	the dictionary of strings to substitute from.
		/// \return	the substituted string, or s if no substitutions could be made.
		//
		static std::string substitute_dict( const std::string& s, const substitute_map_t& d ) {
			std::string new_s( s );
			std::string::size_type start = 0;
			std::string::size_type at = new_s.find( '@', start );
			std::string::size_type semi = new_s.find( ';', at );
			while( (at != std::string::npos) && (semi != std::string::npos) ) {
				//
				// If this token is preceded by a "\" then we just remove
				// the "\" instead of doing any other processing.
				//
				if( (at > 0) && (new_s[at - 1] == '\\') ) {
					at--;
					new_s.erase( at, 1 );
					//
					// If the preceding character was a '\' then we still
					// need to perform substitution on the token.
					//
					if( (at > 0) && (new_s[at - 1] == '\\') ) {
						//
						// Process the token.
						//
						at++;
					} else {
						//
						// Skip the token.
						//
						start = at + 1;
						at = new_s.find( '@', start );
						semi = new_s.find( ';', at );
						continue;
					}
				}
				std::string token = new_s.substr( at + 1, semi - at - 1 );
				std::string replacement;
				adobe::name_t name( token.c_str() );
				substitute_map_t::const_iterator nv_pair = d.find( name );
				if( nv_pair != d.end() ) replacement = nv_pair->second;
				new_s.replace( at, semi - at + 1, replacement );
				at = new_s.find( '@', start );
				semi = new_s.find( ';', at );
			}
			return new_s;
		}

		void update_self() {
			std::string result = substitute_dict( dest_m, subst_map_m );
			if( result == last_m ) return;
			last_m = result;
			if( value_monitor_m ) value_monitor_m( adobe::value_t( result ) );
			if( active_monitor_m ) active_monitor_m( false );
		}
		void update( const adobe::name_t& name, const adobe::value_t& value ) {
			if( value.type() == typeid(std::string) )
				subst_map_m[name] = value.get<std::string>();
			else if( value.type() == typeid(double) ) {
				std::stringstream ss;
				ss << value.get<double>();
				subst_map_m[name] = ss.str();
			}
			if( !initializing_m ) update_self();
		}
		void init() {
			if( !initializing_m ) return;
			adobe::dictionary_t::iterator first( arguments_m.begin() );
			adobe::dictionary_t::iterator last( arguments_m.end() );
			//
			// Build a binder_helper_t for each item in the dictionary.
			// This may be memory-expensive -- must profile.
			//
			while( first != last ) {
				adobe::binder_helper_t* helper( new adobe::binder_helper_t( sheet_m ) );
				adobe::name_t name( first->first );
				adobe::value_t bindable( first->second );
				helper->initialize( bindable,
					boost::bind( &substitute_pump_t::update, this, name, _1 ),
					0 );
				helpers_m.push_back( helper );
				first++;
			}
			initializing_m = false;
		}
	  public:
		substitute_pump_t( const std::string& dest, const adobe::dictionary_t& args, adobe::sheet_t& sheet ) : dest_m( dest ), initializing_m( true ), sheet_m( sheet ), arguments_m( args ) {}
		virtual ~substitute_pump_t() {
			for( helper_list_t::iterator i = helpers_m.begin(); i != helpers_m.end(); i++ )
				delete *i;
		}
		virtual void set( const adobe::value_t& val ) {
			throw std::runtime_error( "substitute data pump is read-only" );
		}
		virtual void set_signals( const adobe::sheet_t::monitor_value_t& monitor_value,
			const adobe::sheet_t::monitor_active_t& monitor_active ) {
			value_monitor_m = monitor_value;
			active_monitor_m = monitor_active;
			last_m = "";
			//
			// Build a binder_helper_t for each item in the dictionary
			// only if we haven't already done so, and if monitor_value
			// is a real function (not zero).
			//
			if( initializing_m && monitor_value ) init();
			if( !initializing_m ) update_self();
		}
		virtual void substitute( const adobe::name_t& name, const adobe::value_t& val ) {
			//
			// We *must* init() before we substitute.
			// 
			if( initializing_m ) init();
			initializing_m = true;
			adobe::for_each( helpers_m,
				boost::bind( &adobe::binder_helper_t::substitute, _1, name, val ) );
			initializing_m = false;
			update_self();
		}
		virtual void invoke() {}
		virtual adobe::binder_ptr_t copy() const {
			return adobe::binder_ptr_t( new substitute_pump_t( dest_m, arguments_m, sheet_m ) );
		}
	};
	adobe::value_t make_substitute_datapump( const std::string& dest, const adobe::dictionary_t& parameters, adobe::sheet_t& sheet ) {
		adobe::binder_ptr_t pump( new substitute_pump_t( dest, parameters, sheet ) );
		return adobe::value_t( pump );
	}
}

